package net.neocat.tcx.speed;

import com.google.common.base.Preconditions;

import static com.google.common.base.Preconditions.checkArgument;
import static java.lang.Math.*;

/**
 * Created with IntelliJ IDEA.
 * User: jschek
 * Date: 5/2/13
 * Time: 8:33 PM
 * To change this template use File | Settings | File Templates.
 */
public class SpeedCalculatorBuilder {

    private static final double cCad = 0.002;
    private double riderHeightMeters;
    private double riderMassKg;
    private double bikeMassKg;
    private double temperatureC;
    private double windSpeedMSec;
    private BikeType bikeType;

    public static void main(String[] args) {
        SpeedCalculator calculator = new SpeedCalculatorBuilder()
                .setBikeType(BikeType.RaceDrops)
                .setRiderHeight(5, 7)
                .setRiderMass(135)
                .setBikeMass(20)
                .setTemperature(68)
                .setWindSpeed(19)
                .create();
        System.out.println(calculator.calculateMilesPerHour(160, 90, -3, toMeters(100)) + " mph");
    }

    private static double toMeters(int inches) {
        return inches * 0.0254;
    }

    private static double toKg(double lbs) {
        return lbs * 0.453592;
    }

    private static double toMetersPerSecond(double mph) {
        return mph * .44704;
    }

    public final SpeedCalculatorBuilder setRiderHeight(int feet, int inches) {
        checkArgument(feet > 0, "feet must be greater than 0.");
        checkArgument(inches >= 0, "inches must be greater than or equal to 0.");
        this.riderHeightMeters = toMeters(feet * 12 + inches);
        return this;
    }

    public final SpeedCalculatorBuilder setRiderHeight(int inches) {
        checkArgument(inches > 0, "inches must be greater than 0.");
        this.riderHeightMeters = toMeters(inches);
        return this;
    }

    public final SpeedCalculatorBuilder setRiderMass(double lbs) {
        checkArgument(lbs > 0, "lbs must be greater than 0.");
        this.riderMassKg = toKg(lbs);
        return this;
    }

    public final SpeedCalculatorBuilder setBikeMass(double lbs) {
        checkArgument(lbs > 0, "lbs must be greater than 0.");
        this.bikeMassKg = toKg(lbs);
        return this;
    }

    public final SpeedCalculatorBuilder setTemperature(double degreesF) {
        checkArgument(degreesF > -100, "degreesF must be greater than -100F.");
        this.temperatureC = 5 * (degreesF - 32) / 9;
        return this;
    }

    public final SpeedCalculatorBuilder setWindSpeed(double mph) {
        this.windSpeedMSec = toMetersPerSecond(mph);
        return this;
    }

    public final SpeedCalculatorBuilder setBikeType(BikeType bikeType) {
        this.bikeType = bikeType;
        return this;
    }

    public SpeedCalculator create() {
        return new DynamicSpeedCalculator();
    }

    public class DynamicSpeedCalculator extends AbstractSpeedCalculator {

        final double totalMassKg = SpeedCalculatorBuilder.this.bikeMassKg + SpeedCalculatorBuilder.this.riderMassKg;
        final BikeType bikeType = SpeedCalculatorBuilder.this.bikeType;
        final double riderHeightMeters = SpeedCalculatorBuilder.this.riderHeightMeters;
        final double temperatureK = SpeedCalculatorBuilder.this.temperatureC + 273;
        final double W = SpeedCalculatorBuilder.this.windSpeedMSec;
        final double CrV = bikeType.TireFDef.Cr;
        final double ATireV = bikeType.TireFDef.ATire;
        final double CrH = bikeType.TireRDef.Cr;
        final double ATireH = bikeType.TireRDef.ATire;
        final double CrEff = bikeType.LoadV * bikeType.CCrV * CrV + (1.0 - bikeType.LoadV) * CrH;
        final double adipos = sqrt(riderMassKg / (riderHeightMeters * 750));
        final double CwaBike = bikeType.CdBike * (bikeType.CATireV * ATireV + bikeType.CATireH * ATireH + bikeType.AFrame);

        public double calculateMetersPerSecond(double powerWatts, int cadenceRpms, double grade, double altitudeMeters) {
            if (powerWatts == 0 && cadenceRpms > 0) {
                throw new IllegalArgumentException("Power is zero, but cadence is non-zero");
            }
            if (powerWatts > 0 && cadenceRpms == 0) {
                throw new IllegalArgumentException("Power is non-zero, but cadence is zero");
            }
            Preconditions.checkArgument(powerWatts >= 0, "power must be >=0");
            Preconditions.checkArgument(cadenceRpms >= 0, "cadenceRpms must be >=0");

            final double slope = atan(grade * 0.01);
            final double CrDyn = 0.1 * cos(slope);
            final double Frg = 9.81 * totalMassKg * (CrEff * cos(slope) + sin(slope));
            final double CwaRider = (1 + cadenceRpms * cCad) * bikeType.Cd * adipos * (((riderHeightMeters - adipos) * bikeType.Sin) + adipos);
            final double Ka = 176.5 * exp(-altitudeMeters * 0.0001253) * (CwaRider + CwaBike) / temperatureK;
            final double cardB = (3 * Frg - 4 * W * CrDyn) / (9 * Ka) - pow(CrDyn, 2) / (9 * pow(Ka, 2)) - (W * W) / 9;
            final double cardA = -((pow(CrDyn, 3) - pow(W, 3)) / 27
                    + W * (5 * W * CrDyn + 4 * pow(CrDyn, 2) / Ka - 6 * Frg) / (18 * Ka)
                    - powerWatts / (2 * Ka * bikeType.Cm)
                    - CrDyn * Frg / (6 * pow(Ka, 2)));
            final double sqrtVal = pow(cardA, 2) + pow(cardB, 3);
            final double ire = cardA - sqrt(sqrtVal);

            double Vms;
            if (sqrtVal >= 0) {
                Vms = pow(cardA + sqrt(sqrtVal), 1 / 3.0) + ((ire < 0) ? -pow(-ire, 1 / 3.0) : pow(ire, 1 / 3.0));
            } else {
                Vms = 2 * sqrt(-cardB) * cos(acos(cardA / sqrt(pow(-cardB, 3))) / 3.0);
            }
            Vms -= 2 * W / 3 + CrDyn / (3 * Ka);
            return Vms;
        }
    }
}
